package com.jgp.sys.ui.annotation.handler;


import com.jgp.common.annotation.UIParam;
import com.jgp.common.utils.JGPUtil;
import com.jgp.common.utils.ReflectUtil;
import com.jgp.sys.ui.ReuseableStream;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.Conventions;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StreamUtils;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.io.PushbackInputStream;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.*;

/**
 * 项目   sys
 * 作者   loufei
 * 时间   2018/3/13
 */

public class UIParamHandler extends RequestResponseBodyMethodProcessor {
    private static final Log logger = LogFactory.getLog(UIParamHandler.class);
    
    private ReuseableStream reuse;
    private String sessionId;
    
    public UIParamHandler(List<HttpMessageConverter<?>> converters) {
        super(converters);
    }
    
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(UIParam.class);
    }
    
    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory
            binderFactory) throws Exception {
        UIParam uiParam = parameter.getParameterAnnotation(UIParam.class);
        String targetPrefix = uiParam.value();
        ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
        MediaType mediaType = inputMessage.getHeaders().getContentType();
        if (StringUtils.isNotBlank(targetPrefix) && Objects.nonNull(mediaType)) {
            parameter = parameter.nestedIfOptional();
            //Type paramType = parameter.getGenericParameterType();
            Class paramClass = parameter.getParameterType();
            //Class<?> contextClass = parameter.getContainingClass();
            String name = Conventions.getVariableNameForParameter(parameter);
    
            Object arg = null;
            if (mediaType.includes(MediaType.APPLICATION_FORM_URLENCODED)) {
                Map<String, Object> tempKeyValue = new HashMap<>();
                webRequest.getParameterNames().forEachRemaining(key -> {
                    String fieldValue = webRequest.getParameter(key);
                    if (key.contains(".")) {
                        String[] items = key.split("\\.");
                        String prefix = items[0];
                        String fieldName = items[1];
                        Class fieldType = ReflectUtil.getDeclaredFieldType(paramClass, fieldName);
                        if (prefix.equals(targetPrefix)) {
                            if (StringUtils.isNotBlank(fieldValue) && Objects.nonNull(fieldType)) {
                                tempKeyValue.put(fieldName, ReflectUtil.cast(fieldValue, fieldType));
                            }
                        }
                    }else {
                        Class fieldType = ReflectUtil.getDeclaredFieldType(paramClass, key);
                        if (StringUtils.isNotBlank(fieldValue) && Objects.nonNull(fieldType)) {
                            tempKeyValue.put(key, ReflectUtil.cast(fieldValue, fieldType));
                        }
                    }
                });
                arg = JGPUtil.toBean(tempKeyValue, paramClass);
            } else if (inputMessage.getHeaders().getContentType().includes(MediaType.APPLICATION_JSON_UTF8)) {
                HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
                Assert.state(servletRequest != null, "No HttpServletRequest");
                //EmptyBodyCheckingHttpInputMessage message = new EmptyBodyCheckingHttpInputMessage(inputMessage);
                if(StringUtils.isBlank(this.sessionId)||!this.sessionId.equals(webRequest.getSessionId())) {
                    this.sessionId = webRequest.getSessionId();
                    this.reuse = null;
                }
                
                if (Objects.isNull(this.reuse)) this.reuse = new ReuseableStream(inputMessage.getBody());
                String json = IOUtils.toString(reuse.open(), "UTF-8");
                Object value = StringUtils.isNotBlank(json) ? JGPUtil.jsonToMap(json).get(uiParam.value()) : new HashMap();
                arg = JGPUtil.toBean(JGPUtil.toJson(value), paramClass);
                this.reuse.reset();
            }
            if (binderFactory != null) {
                if (Objects.nonNull(arg) && Collection.class.isAssignableFrom(arg.getClass())) {
                    Collection collection = (Collection) arg;
                    Iterator iterable = collection.iterator();
                    while (iterable.hasNext()) {
                        WebDataBinder binder = binderFactory.createBinder(webRequest, iterable.next(), name);
                        super.validateIfApplicable(binder, parameter);
                        if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
                            throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
                        }
                        if (mavContainer != null) {
                            mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
                        }
                    }
                } else {
                    WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
                    if (arg != null) {
                        validateIfApplicable(binder, parameter);
                        if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
                            throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
                        }
                    }
                    if (mavContainer != null) {
                        mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
                    }
                }
                
            }
            return adaptArgumentIfNecessary(arg, parameter);
        } else {
            return super.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
        }
        
    }
    
    private Type getHttpEntityType(MethodParameter parameter) {
        Assert.isAssignable(HttpEntity.class, parameter.getParameterType());
        Type parameterType = parameter.getGenericParameterType();
        if (parameterType instanceof ParameterizedType) {
            ParameterizedType type = (ParameterizedType) parameterType;
            if (type.getActualTypeArguments().length != 1) {
                throw new IllegalArgumentException(
                        "Expected single generic parameter on '" + parameter.getParameterName() + "' in method " + parameter.getMethod());
            }
            return type.getActualTypeArguments()[0];
        } else if (parameterType instanceof Class) {
            return Object.class;
        } else {
            return null;
        }
    }
    
    private static class EmptyBodyCheckingHttpInputMessage implements HttpInputMessage {
        
        private final HttpHeaders headers;
        
        @Nullable
        private final InputStream body;
        
        public EmptyBodyCheckingHttpInputMessage(HttpInputMessage inputMessage) throws IOException {
            this.headers = inputMessage.getHeaders();
            InputStream inputStream = inputMessage.getBody();
            if (inputStream.markSupported()) {
                inputStream.mark(1);
                this.body = (inputStream.read() != -1 ? inputStream : null);
                inputStream.reset();
            } else {
                PushbackInputStream pushbackInputStream = new PushbackInputStream(inputStream);
                int b = pushbackInputStream.read();
                if (b == -1) {
                    this.body = null;
                } else {
                    this.body = pushbackInputStream;
                    pushbackInputStream.unread(b);
                }
            }
        }
        
        @Override
        public HttpHeaders getHeaders() {
            return this.headers;
        }
        
        @Override
        public InputStream getBody() {
            return (this.body != null ? this.body : StreamUtils.emptyInput());
        }
        
        public boolean hasBody() {
            return (this.body != null);
        }
    }
}
